/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.web.spring.util;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.AbstractBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.WebApplicationContext;
import org.vacoor.nothing.web.servlet.WebRequestContext;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * 这里实现 BeanPostProcessor 只是为了让该类在所有类初始化之前初始化, 以便在 init 方法中调用
 *
 * @author vacoor
 */
@Component
@Lazy(false)
@SuppressWarnings({"unused"})
public class SpringUtils implements ApplicationContextAware, DisposableBean, BeanPostProcessor {
    static final String WEB_MVC_APPLICATION_CONTEXT_REQUEST_ATTRIBUTE = "org.springframework.web.servlet.DispatcherServlet.CONTEXT";
    /**
     * FrameworkServlet.class + ".CONTEXT." + servletName;
     * {@see org.springframework.web.servlet.FrameworkServlet.SERVLET_CONTEXT_PREFIX}
     * {@see org.springframework.web.servlet.FrameworkServlet#getServletContextAttributeName()}
     */
    static final String WEB_MVC_APPLICATION_CONTEXT_ATTRIBUTE_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";
    static final String ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE = "org.springframework.web.context.WebApplicationContext.ROOT";

    private static ApplicationContext applicationContext;

    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        return (T) getApplicationContext().getBean(name);
    }

    /**
     * 注意代理问题, 如果没有使用接口, 则 spring 使用 cglib 产生代理, 这个时候无法获取被代理类
     */
    public static <T> T getBean(Class<T> requiredType) {
        return getApplicationContext().getBean(requiredType);
    }

    public static <T> T getBean(String name, Class<T> requiredType) {
        return getApplicationContext().getBean(name, requiredType);
    }

    public static <T> Map<String, T> getBeansOfType(Class<T> requiredType) {
        return getApplicationContext().getBeansOfType(requiredType);
    }

    public static Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> annoType) {
        return getApplicationContext().getBeansWithAnnotation(annoType);
    }

    /**
     * 使用该方法需要在 spring context 中配置 {@link org.springframework.context.support.AbstractMessageSource}
     */
    public static String getMessage(String key, Object... args) {
        Locale locale = LocaleContextHolder.getLocale();
        return getApplicationContext().getMessage(key, args, locale);
    }

    /* ******************************************
     *
     * ******************************************/

    public static <T> T autowireAndInitialize(T existingBean, int autowireMode, String beanName) {
        return autowireAndInitialize(getApplicationContext(), existingBean, autowireMode, beanName);
    }

    public static <T> T autowireAndInitialize(ApplicationContext appContext, T existingBean, int autowireMode, String beanName) {
        appContext.getAutowireCapableBeanFactory().autowireBeanProperties(existingBean, autowireMode, false);
        appContext.getAutowireCapableBeanFactory().initializeBean(existingBean, beanName);

        return existingBean;
    }

    /* ******************************************
     *
     * ******************************************/

    public static <T> T register(String beanName, BeanDefinition beanDefinition) {
        return register(getApplicationContext(), beanName, beanDefinition);
    }

    public static <T> T register(ApplicationContext appContext, String beanName, BeanDefinition beanDefinition) {
        BeanDefinitionRegistry beanRegistry = (BeanDefinitionRegistry) appContext.getAutowireCapableBeanFactory();
        try {
            beanRegistry.registerBeanDefinition(beanName, beanDefinition);
        } catch (BeansException e) {
            beanRegistry.removeBeanDefinition(beanName);
            throw e;
        }
        return (T) appContext.getBean(beanName);
    }

    /* *******************************************
     *
     * *******************************************/

    public static Object set(final ApplicationContext applicationContext, final String beanName, String prop, Object value) {
        Map<String, Object> pvs = Maps.newHashMap();
        pvs.put(prop, value);
        return set(applicationContext, beanName, pvs);
    }

    public static Object set(final ApplicationContext applicationContext, final String beanName, Map<String, ?> pvs) {
        return doSet(applicationContext, beanName, applicationContext.getBean(beanName), pvs);
    }

    public static <T> T set(final ApplicationContext applicationContext, final Class<T> requiredType, String prop, Object value) {
        Map<String, Object> pvs = Maps.newHashMap();
        pvs.put(prop, value);
        return set(applicationContext, requiredType, pvs);
    }

    public static <T> T set(final ApplicationContext applicationContext, final Class<T> requiredType, Map<String, ?> pvs) {
        Map<String, T> beansOfType = applicationContext.getBeansOfType(requiredType);
        if (beansOfType.size() > 1) {
            throw new NoUniqueBeanDefinitionException(requiredType);
        }
        Map.Entry<String, T> entry = beansOfType.entrySet().iterator().next();
        return doSet(applicationContext, entry.getKey(), entry.getValue(), pvs);
    }

    public static <T> T set(final ApplicationContext applicationContext, final String beanName, final Class<T> requiredType, String prop, Object value) {
        Map<String, Object> pvs = Maps.newHashMap();
        pvs.put(prop, value);
        return set(applicationContext, beanName, requiredType, pvs);
    }

    public static <T> T set(final ApplicationContext applicationContext, final String beanName, final Class<T> requiredType, Map<String, ?> pvs) {
        return doSet(applicationContext, beanName, applicationContext.getBean(beanName, requiredType), pvs);
    }

    private static <T> T doSet(final ApplicationContext applicationContext, final String beanName, final T existingBean, Map<String, ?> pvs) {
        final AutowireCapableBeanFactory acbf = applicationContext.getAutowireCapableBeanFactory();
        final BeanDefinitionRegistry registry = (BeanDefinitionRegistry) acbf;

        BeanDefinition bd = registry.getBeanDefinition(beanName);
        bd.getPropertyValues().addPropertyValues(pvs);

        // 清除缓存的信息
        // acbf.clearMergedBeanDefinition(beanName);
        // ((DefaultListableBeanFactory) acbf).removeBeanDefinition(beanName); // 该方法会彻底销毁, 不行
        Method m = ReflectionUtils.findMethod(AbstractBeanFactory.class, "clearMergedBeanDefinition", String.class);
        ReflectionUtils.makeAccessible(m);
        ReflectionUtils.invokeMethod(m, acbf, beanName);

        acbf.applyBeanPropertyValues(existingBean, beanName);
        return existingBean;
    }


    /* ******************************************
     *
     * ******************************************/

    /*- bean definition 中值已经被替换了, 除非再搞个记录下
    public static void refreshPropertyResourceConfigurer() {
        // web mvc application context
        ApplicationContext webContext = getWebMvcApplicationContext();
        // root application context
        ApplicationContext rootContext = getRootApplicationContext();
        // static application context
        ApplicationContext staticContext = getStaticApplicationContext();

        if (null != webContext) {
            refreshPropertyResourceConfigurer(webContext);
        }
        if (null != rootContext && webContext != rootContext) {
            refreshPropertyResourceConfigurer(rootContext);
        }
        if (null != staticContext && webContext != staticContext && rootContext != staticContext) {
            refreshPropertyResourceConfigurer(staticContext);
        }
    }

    public static void refreshPropertyResourceConfigurer(ApplicationContext applicationContext) {
        Map<String, PropertyResourceConfigurer> configurersToProcess = applicationContext.getBeansOfType(PropertyResourceConfigurer.class);
        for (PropertyResourceConfigurer configurer : configurersToProcess.values()) {
            if (applicationContext instanceof ConfigurableListableBeanFactory) {
                configurer.postProcessBeanFactory((ConfigurableListableBeanFactory) applicationContext);
            }
            AutowireCapableBeanFactory beanFactory = applicationContext.getAutowireCapableBeanFactory();
            if (beanFactory instanceof ConfigurableListableBeanFactory) {
                configurer.postProcessBeanFactory((ConfigurableListableBeanFactory) beanFactory);
            }
        }
    }
    */

    /*- 我去刷新会引起很多问题 */
    public static void refresh() {
        final List<ApplicationContext> refreshed = Lists.newArrayList();
        refresh(getStaticApplicationContext(), refreshed);
        refresh(getRootApplicationContext(), refreshed);
        refresh(getWebMvcApplicationContext(), refreshed);
    }

    public static void refresh(ApplicationContext context) {
        refresh(context, null);
    }

    public static void refresh(ApplicationContext applicationContext, List<ApplicationContext> ignores) {
        if (null == applicationContext) return;
        ignores = null != ignores ? ignores : Lists.<ApplicationContext>newArrayList();
        ApplicationContext parent = applicationContext.getParent();
        // 必须先刷新父容器
        if (null != parent) {
            refresh(parent, ignores);
        }
        if (!ignores.contains(applicationContext) && applicationContext instanceof ConfigurableApplicationContext) {
            ((ConfigurableApplicationContext) applicationContext).refresh();
            ignores.add(applicationContext);
        } else {
            System.out.println("ignore: " + applicationContext);
        }
    }

    /* ******************************************
     *
     * ******************************************/

    /**
     * 获取 spring application context
     * 按照以下步骤获取
     * 1. 尝试从 request 中获取 web mvc application context
     * 2. 尝试从 servlet context 中获取 dispatcher-servlet name="spring-mvc" 的 web mvc application context
     * 3. 尝试从 servlet context 中获取 root application context
     * 4. 返回静态 application context
     */
    public static ApplicationContext getApplicationContext() {
        ApplicationContext appContext = getWebMvcApplicationContext();
        appContext = null != appContext ? appContext : getRootApplicationContext();
        // 最后使用 static application context
        appContext = null != appContext ? appContext : getStaticApplicationContext();

        if (null == appContext) {
            throw new IllegalStateException("No WebApplicationContext found: not in current context, 你可以在 spring context 中实例化该类来注入一个静态的 application context");
        }
        return appContext;
    }

    public static ApplicationContext getWebMvcApplicationContext() {
        WebRequestContext context = WebRequestContext.getContext();
        if (null == context) {
            return null;
        }
        // 首先获取 web mvc application context
        ApplicationContext appContext = getWebMvcApplicationContext(context.getRequest(), context.getServletContext());
        // 其次获取 root application context
        return null != appContext ? appContext : getRootApplicationContext(context.getServletContext());
    }

    public static ApplicationContext getWebMvcApplicationContext(HttpServletRequest request) {
        return getWebMvcApplicationContext(request, null);
    }

    /**
     * 获取 spring mvc application context
     *
     * @param request
     * @param servletContext
     */
    public static ApplicationContext getWebMvcApplicationContext(HttpServletRequest request, ServletContext servletContext) {
        if (null == request) return null;
        // 首先从请求中获取
        WebApplicationContext context = (WebApplicationContext) request.getAttribute(WEB_MVC_APPLICATION_CONTEXT_REQUEST_ATTRIBUTE);
        // 如果没有, 尝试使用 "spring-mvc" 名称从 servlet context 中获取
        if (null == context && null != servletContext) {
            context = (WebApplicationContext) servletContext.getAttribute(WEB_MVC_APPLICATION_CONTEXT_ATTRIBUTE_PREFIX + "spring-mvc");
        }
        return context;
    }

    public static ApplicationContext getRootApplicationContext() {
        WebRequestContext context = WebRequestContext.getContext();
        return null != context ? getRootApplicationContext(context.getServletContext()) : null;
    }

    /**
     * 获取根 application context
     *
     * @param servletContext
     * @return
     */
    public static ApplicationContext getRootApplicationContext(ServletContext servletContext) {
        return (ApplicationContext) servletContext.getAttribute(ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }

    /**
     * 获取静态 application context
     *
     * @return
     */
    public static ApplicationContext getStaticApplicationContext() {
        return applicationContext;
    }

    public static void setStaticApplicationContext(ApplicationContext applicationContext) {
        SpringUtils.applicationContext = applicationContext;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        setStaticApplicationContext(applicationContext);
    }

    @Override
    public void destroy() throws Exception {
        applicationContext = null;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}